package org.ycsoft.PathComputation;

import java.util.*;
import java.io.*;

import org.ycsoft.MetroidReader.Commons;


/**
 * Encapsulates the data about a city (stops, lines). Can export that information to the Metroid 
 * format
 * @author allad
 *
 */
public class CityMap {
	
	private StopDB stopsCentral = new StopDB();
	private List<Line> lines = new ArrayList<Line>();
	private Map<Stop,ShortestPathTree> shortestPathsTrees = new HashMap<Stop, ShortestPathTree>(); //shortest paths
	
	private boolean isReady = false;  //is set true when the map has loaded all available info
	
	private boolean debug = true; //debug are displayed only if this flag is set true 
	private String mapCityName = "";
	private String mapTransportMean = "";
	private String mapLanguage = "";
	

	
	/**
	 * Initialize all inner properties (stops, line) from an input data file
	 * @param filename Filename to load info from
	 * @return
	 * 
	 */
	public boolean createMap(String filename)
	{
		int i=0;
		String tmp="";
		Stop stopPrinc,stopVois;
		try
		{
			InputStream ips=new FileInputStream(filename); 
			InputStreamReader ipsr=new InputStreamReader(ips);
			BufferedReader br=new BufferedReader(ipsr);
			String ligne;
			Line newLine = null;
			while ((ligne=br.readLine())!=null)
			{
				ligne = ligne.trim();
				if(ligne.length()!=0)
				{
					tmp=ligne.toLowerCase();
					//i 0 for lines context
					//i 1 for stops
					if(tmp.startsWith(Commons.PREFIX+"city") && tmp.contains(Commons.SEPARATOR+"") && tmp.split(Commons.SEPARATOR+"").length >=2)
					{
						mapCityName=ligne.split(Commons.SEPARATOR+"")[1];
						trace("City: "+mapCityName);
						continue;
					}
					if(tmp.startsWith(Commons.PREFIX+"mean") && tmp.contains(Commons.SEPARATOR+"")&& tmp.split(Commons.SEPARATOR+"").length >=2)
					{
						mapTransportMean=ligne.split(Commons.SEPARATOR+"")[1];
						trace("Mean: "+mapTransportMean);
						continue;
					}
					if(tmp.startsWith(Commons.PREFIX+"language") && tmp.contains(Commons.SEPARATOR+"") && tmp.split(Commons.SEPARATOR+"").length >=2)
					{
						mapLanguage=ligne.split(Commons.SEPARATOR+"")[1];
						trace("Language: "+mapLanguage);
						continue;
					}
					if(tmp.startsWith(Commons.PREFIX+"totaltime"))
					{
						//add later details on its storage
						if(tmp.contains(Commons.SEPARATOR+"") && tmp.split(Commons.SEPARATOR+"").length >=2){
							String part2 = tmp.split(Commons.SEPARATOR+"")[1];
							part2 = part2.trim();
							try{
								int total = Integer.parseInt(part2);
								newLine.setTotalTime(total*60);
							}catch(NumberFormatException nfe){
								newLine.setTotalTime(-1);
							}
						}
						continue;
					}
					
					if(tmp.startsWith(Commons.PREFIX+"frequency"))
					{
						//add later details on its storage
						if(tmp.contains(Commons.SEPARATOR+"") && tmp.split(Commons.SEPARATOR+"").length >=2){
							String part2 = tmp.split(Commons.SEPARATOR+"")[1];
							part2 = part2.trim();
							try{
								int freq = Integer.parseInt(part2);
								newLine.setFrequency(freq*60);
							}catch(NumberFormatException nfe){
								newLine.setFrequency(-1);
							}
						}
						continue;
					}
					if(tmp.startsWith(Commons.UNIDIRECTIONAL)){
						if(newLine != null){
							newLine.setBidirectional(false);
						}
					}
					if(tmp.startsWith(Commons.PREFIX+"line"))
					{
						String linename = ligne.substring(5).trim();
						if(ligne.contains(Commons.SEPARATOR+"")){
							linename = ligne.split(Commons.SEPARATOR+"")[1];
						}
						//end previous line
						if(newLine != null){
							newLine.endStops();
						}
						newLine=new Line(linename);
						lines.add(newLine );
						i=0;
						trace(newLine);
						continue;
					}
					if(tmp.startsWith(Commons.PREFIX+"stops"))
					{
						i=1;
						continue;
					}
					if(i==1)
					{
						//chps lignes
						String voisins="";
						String stop="";
						if(ligne.indexOf('(')>0)
						{
							//fields du split chp et chp2 pr les voisins;
							String[] chp;
							chp=ligne.split("\\(");
							stop=chp[0];
							stop = stop.trim();
							
							//Création stop
							stopPrinc = stopsCentral.addStop(stop);	
							
							//ajout a la ligne en cours
							newLine.addStop(stopPrinc);
							
							//Creer arete et l'ajouter a la liste des voisins du stop
							crNormalEdge(newLine, stopPrinc);
							
							
							trace(stopPrinc);
							
							voisins = chp[1];
							
							voisins = voisins.trim();
							
							//supprimer la parenthèse fermante
							if(voisins.endsWith(")")){							
								voisins = voisins.substring(0, voisins.length()-1);
							}
							
							//Si des voisins sont spécifiés...
							if(voisins.indexOf(',')>0)
							{
								String[] champ2;
								champ2 = voisins.split(",");
								
								for(int ind=0;ind<champ2.length;ind++){
									String voisin = champ2[ind];
									//Verifier si duree specifiee
									if(voisin.indexOf(':')>0)
									{
										stop=(voisin.split(":"))[0];
										stop = stop.trim();
										String len; //length of the edge
										len = (voisin.split(":"))[1];
										len = len.trim();
										int length; //length value 
										try{
											length = Integer.parseInt(len);										
										}catch(NumberFormatException nfe){
											//l'utilisateur n'a pas entré un nombre correct
											length = 5;
										}
										stopVois=stopsCentral.addStop(stop);
										trace(stopVois);

										//creation de l'arete
										crFootEdge(stopPrinc, stopVois,length);
									}
									else//Pas de duree spécifiée
									{
										stop=voisin;
										stop = stop.trim();
										stopVois=stopsCentral.addStop(stop);
										trace(stopVois);

										//creation de l'arete
										crFootEdge(stopPrinc, stopVois,-1);
									}
								}
																
								
							}
														
						}
						else //Si pas de voisin joignable à pied specifiée
						{
							stop=ligne;
							stop = stop.trim();
							
							//Création stop
							stopPrinc = stopsCentral.addStop(stop);	
							
							//ajout à la ligne en cours
							newLine.addStop(stopPrinc);
							
							//Créer arete et l'ajouter à la liste des voisins du stop
							crNormalEdge(newLine, stopPrinc);
							
							
							trace(stopPrinc);
						}
					}	
				}
			}
			br.close(); 
			//close currentLine
			if(newLine != null)
				newLine.endStops();
		}		
		catch (Exception e)
		{
			System.out.println(e.toString());
		}
		isReady=true;
		trace(this.getStops().size());

		return false;
	}
	
	/**
	 * @author Look
	 * @param myline the current line which includes the edge
	 * @param mystop Current stop 
	 * @return
	 */
	private boolean crNormalEdge(Line myline, Stop mystop)
	{
		int nbStop;
		/*Ce n'est ps le premier stop de la ligne
		On crée donc une arete partant du précédent vers le 
		current stop_princ */
		if((nbStop=myline.getStops().size())>1)
		{
			//System.out.println(nb_stop);
			Stop departure=myline.getStops().get(nbStop-2);
			Edge edge=new Edge(myline,departure,mystop,-1);
			Edge backEdge = new Edge(myline.getInverseLine(),mystop,departure,-1);

			//on ajoute l'arete crée au voisinage du stop
			departure.addEdge(edge);
			if(myline.isBidirectional()){
				mystop.addEdge(backEdge);
			}
			else mystop.addIncomingEdge(edge); //used to retrieve list of all lines passing by stop later on
			return true;
		}
		else
			return false;
	}
	
	/**
	 * Adds a foot edge to a stop
	 * @author Look
	 * @param dep departure Stop
	 * @param targ target Stop
	 * @param length Length of the edge
	 */
	private void crFootEdge(Stop dep,Stop targ,int length)
	{
		Edge edge;
		Edge backEdge;
		if(length == -1){
			edge =new Edge(null,dep,targ,Commons.DEFAULT_EDGE_LENGTH); //if not specified the length is by default 5 min
			backEdge = new Edge(null,targ,dep,Commons.DEFAULT_EDGE_LENGTH); 
		}
		else {
			edge = new Edge(null,dep,targ,length*60);
			backEdge = new Edge(null,targ,dep,length*60);
		}
		//afoot, we can walk both ways
		dep.addEdge(edge);
		targ.addEdge(backEdge);
	}
	
	
	/**
	 * Displays messages and serves as trace utility
	 * @param o message to trace (ie to display)
	 */
	private void trace(Object o){
		if(debug){
			if(o instanceof Stop){
				Stop s =(Stop)o;
				System.out.println(s+"|"+s.getStopId());
			}
			else System.out.println(o);
		}
		
	}
	
	
	
	/**
	 * @return The StopDB object containing all stops
	 */
	public StopDB getStops(){
		return stopsCentral;
	}
	
	/**
	 * @return The number of lines in this map
	 */
	public int getLineCount(){
		return lines.size();
	}

	
	/**
	 * @return number of stops
	 */
	public int getStopsCount()
	{
		return stopsCentral.getStopsSet().size();
	}
	
	/**
	 * Need in Sorting lexicographically all the stops name
	 */
	private class SortMyStops implements Comparator<String>
	{
		public int compare(String s1, String s2)
		{
			s1 = s1.toLowerCase();
			return s1.compareTo(s2.toLowerCase());
		}
	}
	
	
	/**
	 * Export all info to a file in the Metroid format
	 * @param filename Name of the metroid file to save
	 */
	
	public void exportToMetroid(String filename) throws IOException
	{
		int i=0; //indice for sortedStops array
		long position=0; //var for position maintenance
		String[] sortedStops=new String[this.getStopsCount()]; 
		CityMap map = this;
		Map<Integer,Long> linePosition= new HashMap<Integer,Long>();
		long[] stopPosition=new long[map.getStopsCount()];
		long[] stopSPTPosition=new long[map.getStopsCount()];
		long indexStopsPosition, indexLinesPosition, indexStopsNamesPosition;
		
		String metroidName = filename;
		if(metroidName == null){
			metroidName=map.getMapCityName()+"_"+map.getMapMean();
		}
		FileOutputStream metroidFile = new FileOutputStream(metroidName);
		//Let's talk about our metroid file
			/**Write lines to metroid file
			 * Save the position of details on each line in array
			 * maintaining the current position in the file
			 */
			for(Line line:map.lines)
			{
				linePosition.put(line.getLineId(),position);  //ids start from 1
				position+=writeDown(line.toMetroid(), metroidFile);
				if(line.isBidirectional()){
					//inverse line
					linePosition.put(line.getInverseLine().getLineId(),position);
					position += writeDown(line.getInverseLine().toMetroid(),metroidFile);
				}
			}
			
			/**Write stops to metroid file
			 * Save the position of details on each stop in array
			 * maintaining the current position in the file
			 */
			for(Stop stop:map.stopsCentral.getStopsSet())
			{
				stopPosition[stop.getStopId()-1]=position; //ids start from 1
				position+=writeDown(stop.toMetroid(), metroidFile);
			}
			
			/**Write ShortestPathTrees for each file to metroid file
			 * Save the position of details on each ShortestPathTrees in array 
			 * maintaining the current position in the file
			 */
			for(Stop stop:map.stopsCentral.getStopsSet())
			{
				stopSPTPosition[stop.getStopId()-1]=position;
				position+=map.shortestPathsTrees.get(stop).toMetroid(metroidFile);
				//Build array useful for stopId<->stopName table
				sortedStops[i]=stop.getStopName();
				i++;
			}
			//Save position of stops and their ShortestPathTrees
			indexStopsPosition=position;
	        //Starting Index stops
			//build index "stop:position,short..position" like
			position+=writeDown(getIndexStops(map, stopPosition, stopSPTPosition), 
					metroidFile);
			//end of indexStops
			//Save position of  lines details
			indexLinesPosition=position;
			//Starting Index lines
			//build index "line:position" like
			position+=writeDown(getIndexLines(map, linePosition),metroidFile); 
			//end of indexStops
			//Save position of stopId<->name table
			indexStopsNamesPosition=position;
			position+=writeDown("indexstopsnames"+Commons.LINE_SEPARATOR, metroidFile);
			//Sort lexico Stop's name and write'em down
			Arrays.sort(sortedStops, new SortMyStops());
			for(String stopName:sortedStops)
			{
				//TODO faster format for stops names index
				position+=writeDown(stopName+":"+
					map.stopsCentral.getStop(stopName).getStopId()+Commons.LINE_SEPARATOR,
					metroidFile);				
			}
			position+=writeDown("end"+Commons.LINE_SEPARATOR, metroidFile);
			//End of stopId<->name table
			//Place now aux metadatas
			position+=writeDown("metadata"+Commons.LINE_SEPARATOR,metroidFile);
			position+=writeDown("city"+Commons.SEPARATOR+map.getMapCityName()+Commons.LINE_SEPARATOR, metroidFile);
			position+=writeDown("lang"+Commons.SEPARATOR+map.getMapLanguage()+Commons.LINE_SEPARATOR, metroidFile);
			position+=writeDown("mean"+Commons.SEPARATOR+map.getMapMean()+Commons.LINE_SEPARATOR, metroidFile);
			Date now = new Date();
			position+=writeDown("version"+Commons.SEPARATOR+now.toGMTString()+Commons.LINE_SEPARATOR, metroidFile); //TODO better versioning
			position+=writeDown("author"+Commons.SEPARATOR+"Cherif YAYA"+Commons.LINE_SEPARATOR, metroidFile); //TODO better authoring
			position+=writeDown("numberstops"+Commons.SEPARATOR+stopsCentral.size()+Commons.LINE_SEPARATOR, metroidFile);
			position+=writeDown("numberlines"+Commons.SEPARATOR+lines.size()+Commons.LINE_SEPARATOR, metroidFile);
			//TODO number lines
			position+=writeDown("indexstops"+Commons.SEPARATOR+Commons.formatNumber(indexStopsPosition,Commons.POSITION_DIGITS_NB)+Commons.LINE_SEPARATOR, metroidFile);
			position+=writeDown("indexlines"+Commons.SEPARATOR+Commons.formatNumber(indexLinesPosition,Commons.POSITION_DIGITS_NB)+Commons.LINE_SEPARATOR, metroidFile);
			position+=writeDown("indexstopsname"+Commons.SEPARATOR+Commons.formatNumber(indexStopsNamesPosition,Commons.POSITION_DIGITS_NB)+Commons.LINE_SEPARATOR, metroidFile);
			
			position+=writeDown("end"+Commons.LINE_SEPARATOR,metroidFile);
			//close the file stream
			metroidFile.close();
	}
	
	/**
	 * Writes down byte to the metroid file 
	 * @return number of bytes written
	 */
	public static long writeDown(String temp, FileOutputStream fos) throws IOException
	{
		long longueur=0;
		byte[] tmp;  //Contains byte version of wat's goin 2d file

		tmp=temp.getBytes(Commons.DEFAULT_ENCODING); //get utf-8 bytes
		longueur=tmp.length;
		fos.write(tmp);
		fos.flush();
		
		return longueur;
	}
	
	/**
	 * @return the indexStops (byte[] format)
	 * @param map, stop's position array, stop's shortestPathTree's array
	 */	
	public String getIndexStops(CityMap map, long[] stopPosition, long[]stopSPTPosition)
	{
		String indexStops="indexstops"+Commons.LINE_SEPARATOR;

		for(Stop stop:map.stopsCentral.getStopsSet())
		{
			//stopId : stops record postion,SPT position
			indexStops = indexStops.concat(Commons.formatNumber(stop.getStopId(),Commons.ID_DIGITS_NB)+":"+
					Commons.formatNumber(stopPosition[stop.getStopId()-1],Commons.POSITION_DIGITS_NB)+","
					+Commons.formatNumber(stopSPTPosition[stop.getStopId()-1],Commons.POSITION_DIGITS_NB)+Commons.LINE_SEPARATOR);			
		}
		
		indexStops += "end"+Commons.LINE_SEPARATOR;
		return indexStops;
	}
	
	/**
	 * @Return the indexLines (byte[] format)
	 * @param map, line position array
	 */	
	public String getIndexLines(CityMap map, Map<Integer,Long> linePosition)
	{
		String indexLines="indexlines"+Commons.LINE_SEPARATOR;
		for(Integer line:linePosition.keySet())
		{
			indexLines = indexLines.concat(Commons.formatNumber(line,Commons.ID_DIGITS_NB)+":"+
					Commons.formatNumber(linePosition.get(line),Commons.POSITION_DIGITS_NB) +Commons.LINE_SEPARATOR);
		}
		indexLines += "end"+Commons.LINE_SEPARATOR;
		return indexLines;
		
	}
	
	
	/**
	 * Runs the Dijkstra algorithm on each stop to build the shortest paths trees
	 */
	public void computeShortestPaths(){
		for(Stop stop : stopsCentral.getStopsSet()){
			//compute shortest paths tree for that stop
			//and store it
			shortestPathsTrees.put(stop, computeShortestPathsTree(stop)); 
		}
	}
	
	/**
	 * Runs the Dijkstra algorithm on the stop to build it shortest paths trees
	 */
	public ShortestPathTree computeShortestPathsTree(Stop root)
	{
		StopPriorityQueue prioQueue=new StopPriorityQueue(stopsCentral.size());
		prioQueue.buildHeap(stopsCentral, root);//Initializing the heap
		
		//Mapping from stops to their previous
		ShortestPathTree pathTree = new ShortestPathTree(root,stopsCentral);

		while(!prioQueue.isEmpty())
		{
			//extract min
			PriorityQueueItem stopMin=prioQueue.extractMin();
			for(Edge edge : stopMin.getStop().getEdges()){
				int alt = stopMin.getDistance() + edge.getLength();
				//add connection cost (if any)
				Line previousLine = pathTree.getBackEdgeLine(stopMin.getStop());
				//on s'assure qu'aucune des 2 lignes n'est en realite un trajet a pied et qu'elles sont différentes
				if(previousLine != null && edge.getLine() != null && !previousLine.equals(edge.getLine()))
				{
					//ok. it is a connection. add default cost
					alt += Commons.DEFAULT_CONNECTION_LENGTH;
				}
				if(alt < pathTree.getDistance(edge.getArrival())){
					//if this new path is better than what is currently know then it's our new best choice
					pathTree.putDistance(edge.getArrival(), alt);
					pathTree.putPrevious(edge.getArrival(),stopMin.getStop());
					pathTree.putBackEdgeLine(edge.getArrival(), edge.getLine());
					//update the key in the heap
					prioQueue.changeKey(edge.getArrival(), alt);
				}
			}
		}
		
		return pathTree;

	}
	
	/**
	 * 
	 * @return map's City name
	 */
	public String getMapCityName()
	{
		return this.mapCityName;
	}
	
	/**
	 * 
	 * @return map's Language
	 */
	public String getMapLanguage()
	{
		return this.mapLanguage;
	}
	
	/**
	 * @return map's mean
	 */
	
	public String getMapMean()
	{
		return this.mapTransportMean;
	}
}

